package org.veloxbot.api.methods;

import org.veloxbot.api.internals.concurrent.Task;

import org.veloxbot.api.utils.Random;

import org.veloxbot.ui.Chrome;

import java.awt.Point;
import java.awt.Component;

import java.awt.event.MouseEvent;

import java.util.List;
import java.util.ArrayList;

/**
 * @author Velox
 */
public class Mouse {

	private static int x = -1;
	private static int y = -1;

	private static double speed = 10D;

	private static Point target = null;

	/**
	 * Gets the mouse's X location.
	 *
	 * @return The mouse's X location.
	 */
	public static int getX() {
		return x;
	}

	/**
	 * Gets the mouse's Y location.
	 *
	 * @return The mouse's Y location.
	 */
	public static int getY() {
		return y;
	}

	/**
	 * Gets the location of the mouse.
	 *
	 * @return The location of the mouse.
	 */
	public static Point getLocation() {
		return new Point(x, y);
	}

	/**
	 * Gets the point in which the mouse is moving to.
	 *
	 * @return The point the mouse is moving to.
	 */
	public static Point getTargetLocation() {
		return target;
	}

	/**
	 * Sets the speed of the mouse.
	 *
	 * @param speed The speed in which you want to set the mouse's speed to.
	 */
	public static void setSpeed(final double speed) {
		if (speed < 0 || speed > 10) {
			throw new IndexOutOfBoundsException("Speed should be between 0 and 10");
		}
		Mouse.speed = speed;
	}

	/**
	 * Gets the mouse's speed.
	 *
	 * @return The mouse's speed.
	 */
	public static double getSpeed() {
		return speed;
	}

	/**
	 * Gets the mouse's last clicked location.
	 *
	 * @return The mouse's last clicked location.
	 */
	public static Point getPressLocation() {
		return Chrome.bot.nazi.pressLocation;
	}

	/**
	 * Gets the time the mouse last clicked.
	 *
	 * @return The time the mouse last clicked.
	 */
	public static long getPressTime() {
		return Chrome.bot.nazi.pressTime;
	}

	private static Component getDispatchTarget() {
		return Chrome.bot.getDispatchTarget();
	}

	private static MouseEvent createMouseEvent(final int x, final int y, final int event) {
		return new MouseEvent(getDispatchTarget(), event, System.currentTimeMillis(), 0, x, y, 0, false);
	}

	/**
	 * Clicks the mouse button corresponding to the given parameter.
	 *
	 * @param left <t>true</t> to click left, otherwise right.
	 */
	public static void click(final boolean left) {
		final MouseEvent enter = new MouseEvent(getDispatchTarget(), MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), 0, x, y, 1, false, left ? MouseEvent.BUTTON1 : MouseEvent.BUTTON3);
		final MouseEvent exit = new MouseEvent(getDispatchTarget(), MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(), 0, x, y, 1, false, left ? MouseEvent.BUTTON1 : MouseEvent.BUTTON3);
		Chrome.bot.nazi.redispatch(enter);
		Task.sleep(25, 50);
		Chrome.bot.nazi.redispatch(exit);
	}

	/**
	 * Sets the location of the mouse relative to the given parameters.
	 *
	 * @param x The X location to set the mouse to.
	 * @param y The Y location to set the mouse to.
	 */
	public static void hop(final int x, final int y) {
		if (!Game.isPointValid(x, y)) {
			return;
		}
		Mouse.x = x;
		Mouse.y = y;
		Chrome.bot.nazi.redispatch(createMouseEvent(x, y, MouseEvent.MOUSE_MOVED));
	}

	/**
	 * Sets the location of the mouse relative to the given parameter.
	 *
	 * @param point The location to set th mouse to.
	 */
	public static void hop(final Point point) {
		if (point == null) {
			return;
		}
		hop(point.x, point.y);
	}

	/**
	 * @author BenLand100
	 */
	private static synchronized Point[] getPathTo(final Point target, final double gravity, double wind, double maxStep, final double targetArea) {
		final List<Point> path = new ArrayList<Point>();
		if (target == null || !Game.isPointValid(target)) {
			return path.toArray(new Point[path.size()]);
		}
		final double sqrt3 = Math.sqrt(3);
		final double sqrt5 = Math.sqrt(5);
		double dist, veloX = 0, veloY = 0, windX = 0, windY = 0;
		final Point origin = getLocation();
		while ((dist = Math.hypot(origin.x - target.x, origin.y - target.y)) >= 1) {
			wind = Math.min(wind, dist);
			if (dist >= targetArea) {
				windX = windX / sqrt3 + (Math.random() * (wind * 2D + 1D) - wind) / sqrt5;
				windY = windY / sqrt3 + (Math.random() * (wind * 2D + 1D) - wind) / sqrt5;
			} else {
				windX /= sqrt3;
				windY /= sqrt3;
				if (maxStep < 3) {
					maxStep = Math.random() * 3 + 3D;
				} else {
					maxStep /= sqrt5;
				}
			}
			veloX += windX + gravity * (target.x - origin.x) / dist;
			veloY += windY + gravity * (target.y - origin.y) / dist;
			final double veloMag = Math.hypot(veloX, veloY);
			if (veloMag > maxStep) {
				final double randomDist = maxStep / 2D + Math.random() * maxStep / 2D;
				veloX = (veloX / veloMag) * randomDist;
				veloY = (veloY / veloMag) * randomDist;
			}
			origin.x += veloX;
			origin.y += veloY;
			final int mx = Math.round(origin.x);
			final int my = Math.round(origin.y);
			if (Mouse.x != mx || Mouse.y != my) {
				path.add(new Point(mx, my));
			}
		}
		return path.toArray(new Point[path.size()]);
	}

	/**
	 * Moves the mouse relative to the given parameters.
	 *
	 * @param x The X location to move the mouse to.
	 * @param y The Y location to move the mouse to.
	 */
	public static synchronized void move(final int x, final int y) {
		if (!Game.isPointValid(x, y)) {
			return;
		}
		target = new Point(x, y);
		final double pace = 0D + (Mouse.speed + Random.nextDouble(3D, 6D));
		final double speed = (Math.random() * pace + pace) / (pace / 2D);
		for (final Point p : getPathTo(new Point(x, y), 9D, Random.nextDouble(0D, 3D), 10D * speed, 8D * speed)) {
			hop(p);
			Task.sleep(20, 30);
		}
		target = null;
	}

	/**
	 * Moves the mouse relative to the given parameter.
	 *
	 * @param point The location to move the mouse to.
	 */
	public static synchronized void move(final Point point) {
		if (!Game.isPointValid(x, y)) {
			return;
		}
		move(point.x, point.y);

	}

	/**
	 * Moves the mouse to to given location, and clicks the mouse button corresponding to the given parameter.
	 *
	 * @param x    The X location to move the mouse to.
	 * @param y    The Y location to move the mouse to.
	 * @param left <t>true</t> to click left, otherwise right.
	 */
	public static synchronized void click(final int x, final int y, final boolean left) {
		if (!Game.isPointValid(x, y)) {
			return;
		}
		move(x, y);
		click(left);
	}

	/**
	 * Moves the mosue to the given location, and clicks the mouse button corresponding to the given parameter.
	 *
	 * @param target The location to move the mouse to.
	 * @param left   <t>true</t> to click left, otherwise right.
	 */
	public static synchronized void click(final Point target, final boolean left) {
		if (target == null || !Game.isPointValid(target)) {
			return;
		}
		click(target.x, target.y, left);
	}
}